local constants = require "kong.constants"


local kong = kong
local type = type


local _realm = 'Key realm="' .. _KONG._NAME .. '"'


local KeyAuthHandler = {}


KeyAuthHandler.PRIORITY = 1003
KeyAuthHandler.VERSION = "2.1.0"


local function load_credential(key)
    local cred, err = kong.db.keyauth_credentials:select_by_key(key)
    if not cred then
        return nil, err
    end

    return cred, nil, cred.ttl
end


local function set_consumer(consumer, credential)
    local set_header = kong.service.request.set_header
    local clear_header = kong.service.request.clear_header

    if consumer and consumer.id then
        set_header(constants.HEADERS.CONSUMER_ID, consumer.id)
    else
        clear_header(constants.HEADERS.CONSUMER_ID)
    end

    if consumer and consumer.custom_id then
        set_header(constants.HEADERS.CONSUMER_CUSTOM_ID, consumer.custom_id)
    else
        clear_header(constants.HEADERS.CONSUMER_CUSTOM_ID)
    end

    if consumer and consumer.username then
        set_header(constants.HEADERS.CONSUMER_USERNAME, consumer.username)
    else
        clear_header(constants.HEADERS.CONSUMER_USERNAME)
    end

    kong.client.authenticate(consumer, credential)

    if credential then
        if credential.username then
            set_header(constants.HEADERS.CREDENTIAL_USERNAME, credential.username)
        else
            clear_header(constants.HEADERS.CREDENTIAL_USERNAME)
        end

        clear_header(constants.HEADERS.ANONYMOUS)

    else
        clear_header(constants.HEADERS.CREDENTIAL_USERNAME)
        set_header(constants.HEADERS.ANONYMOUS, true)
    end
end


local function do_authentication(conf)
    if type(conf.key_names) ~= "table" then
        kong.log.err("no conf.key_names set, aborting plugin execution")
        return nil, { status = 500, message = "Invalid plugin configuration" }
    end

    local headers = kong.request.get_headers()
    local query = kong.request.get_query()
    local key
    local body

    -- read in the body if we want to examine POST args
    if conf.key_in_body then
        local err
        body, err = kong.request.get_body()

        if err then
            kong.log.err("Cannot process request body: ", err)
            return nil, { status = 400, message = "Cannot process request body" }
        end
    end

    -- search in headers & querystring
    for i = 1, #conf.key_names do
        local name = conf.key_names[i]
        local v = headers[name]
        if not v then
            -- search in querystring
            v = query[name]
        end

        -- search the body, if we asked to
        if not v and conf.key_in_body then
            v = body[name]
        end

        if type(v) == "string" then
            key = v

            if conf.hide_credentials then
                query[name] = nil
                kong.service.request.set_query(query)
                kong.service.request.clear_header(name)

                if conf.key_in_body then
                    body[name] = nil
                    kong.service.request.set_body(body)
                end
            end

            break

        elseif type(v) == "table" then
            -- duplicate API key
            return nil, { status = 401, message = "Duplicate API key found" }
        end
    end

    -- this request is missing an API key, HTTP 401
    if not key or key == "" then
        kong.response.set_header("WWW-Authenticate", _realm)
        return nil, { status = 401, message = "No API key found in request" }
    end

    -- retrieve our consumer linked to this API key

    local cache = kong.cache

    local credential_cache_key = kong.db.keyauth_credentials:cache_key(key)
    local credential, err = cache:get(credential_cache_key, nil, load_credential,
        key)
    if err then
        kong.log.err(err)
        return kong.response.exit(500, {
            message = "An unexpected error occurred"
        })
    end

    -- no credential in DB, for this key, it is invalid, HTTP 401
    if not credential then
        return nil, { status = 401, message = "Invalid authentication credentials" }
    end

    -----------------------------------------
    -- Success, this request is authenticated
    -----------------------------------------

    -- retrieve the consumer linked to this API key, to set appropriate headers
    local consumer_cache_key, consumer
    consumer_cache_key = kong.db.consumers:cache_key(credential.consumer.id)
    consumer, err = cache:get(consumer_cache_key, nil,
        kong.client.load_consumer,
        credential.consumer.id)
    if err then
        kong.log.err(err)
        return nil, { status = 500, message = "An unexpected error occurred" }
    end

    set_consumer(consumer, credential)

    return true
end


function KeyAuthHandler:access(conf)
    -- check if preflight request and whether it should be authenticated
    if not conf.run_on_preflight and kong.request.get_method() == "OPTIONS" then
        return
    end

    if conf.anonymous and kong.client.get_credential() then
        -- we're already authenticated, and we're configured for using anonymous,
        -- hence we're in a logical OR between auth methods and we're already done.
        return
    end

    local ok, err = do_authentication(conf)
    if not ok then
        if conf.anonymous then
            -- get anonymous user
            local consumer_cache_key = kong.db.consumers:cache_key(conf.anonymous)
            local consumer, err = kong.cache:get(consumer_cache_key, nil,
                kong.client.load_consumer,
                conf.anonymous, true)
            if err then
                kong.log.err("failed to load anonymous consumer:", err)
                return kong.response.exit(500, { message = "An unexpected error occurred" })
            end

            set_consumer(consumer, nil)

        else
            return kong.response.exit(err.status, { message = err.message }, err.headers)
        end
    end
end


return KeyAuthHandler
